Starting Assumptions

We're going to model the cost of one specific home, and compare it to an equivalent rent. The house is here. Pretty much everything on that page is all the reasons why I don't want to live in the Bay Area.

Anyway, here's the rent estimates:


In [1]:
rent_per_month = 3995
rent_increase_per_month = 100.0 / 12

Zillow has also helpfully calculated the 30-year fixed mortgage terms:


In [2]:
price = 899000
down = 179800
apr = 0.03298
term = 12 * 30
yearly_taxes = 4908

Holy hell. Let's get started.

MATH TIME

First, the fundamental law of finance. Net present value of any sum of money:


In [3]:
def npv(future_value, discount_rate, t):
    return future_value / (1 + discount_rate) ** t

Calculating the net present value of all rental costs is fairly simple. Assume we're paying one month's rent as a deposit, and discount each month's rent:


In [4]:
def rent(months, r):
    def cost_of_month(t):
        return rent_per_month + (rent_increase_per_month * t)
    deposit = rent_per_month
    deposit_back = npv(deposit, r, months)
    return -deposit + sum(-npv(cost_of_month(t), r, t) for t in xrange(0, months)) + deposit_back

So, after 10 years, we will pay:


In [5]:
rent(12 * 10, 0)


Out[5]:
-538900.0

Checks out.

Calculating the cost of buying is obviously more involved. Let's start with some basic mortgage definitions:


In [6]:
class Mortgage:
    def __init__(self, down, total, payments, rate):
        self.down, self.total, self.payments, self.rate = down, total, payments, rate
        
    def monthly_payment(self):
        r = self.rate
        P = self.total - self.down
        N = self.payments
        # From https://en.wikipedia.org/wiki/Mortgage_calculator#Monthly_payment_formula
        return (r * P)  / (1 - (1 + r) ** -N)
        
    def remaining(self, t):
        balance = self.total - self.down
        for _ in xrange(0, t):
            interest = balance * self.rate
            balance -= (self.monthly_payment() - interest)
        return max(0, balance)

And plug in the terms Zillow gave us:


In [7]:
mortgage = Mortgage(down, price, term, apr / 12)

Now let's do some basic sanity checking:


In [8]:
mortgage.monthly_payment()


Out[8]:
3148.981815478768

In [9]:
mortgage.remaining(12 * 30)


Out[9]:
6.8953340814914554e-09

In [10]:
mortgage.remaining(0)


Out[10]:
719200

All looking good.

Now we can model the cost of buying. We need to pay:

  • money down for the mortgage
  • closing costs on purchase, including agent fees, inspection, etc
  • closing costs on sale
  • monthly maintenance, monthly mortgage
  • taxes (there are actually some important tax deductions you can claim against mortgage interest and PMI, but we're going to ignore those for simplicity)

In [11]:
def buy_in_general(mortgage, buy_closing_costs, sale_closing_costs, maintenance, taxes, months, appreciation, r):
    cost_to_close = mortgage.down + (mortgage.total * buy_closing_costs)
    
    mortgage_cost = sum(npv(mortgage.monthly_payment(), r, t) for t in xrange(0, min(months, mortgage.payments)))
    maintenance = sum(npv(maintenance * mortgage.total / 12, r, t) for t in xrange(0, months))
    
    taxes = sum(npv(taxes, r, year * 12) for year in xrange(months / 12))
    
    owed_bank = mortgage.remaining(months)
    from_buyer = mortgage.total * appreciation
    cash_at_sale = from_buyer - owed_bank - (from_buyer * sale_closing_costs)
    
    return -cost_to_close - mortgage_cost - maintenance - taxes + npv(cash_at_sale, r, months)

Let's assume:

  • Closing costs are 1% on buying, and 6.5% on sale. Costs are dramatically higher on sale because the sale transaction is typically structured so that agent commissions are "paid" by the seller out of the price of the home.
  • Maintenance is 1% per year.
  • The home does not appreciate in value whatsoever during the term of the investment (which is arguably pessimistic).

In [12]:
def buy(months, r): 
    return buy_in_general(mortgage, 0.01, 0.065, 0.01, yearly_taxes, months, 1.0, r)

Let's see how much this house will cost us, undiscounted, over several years:


In [13]:
[buy(y * 12, 0) for y in xrange(1, 11)]


Out[13]:
[-104827.5976196698,
 -141751.9497335152,
 -178182.04319200414,
 -214101.32867546898,
 -249492.7027414592,
 -284338.48927098454,
 -318620.4202935202,
 -352319.6161699728,
 -385416.5651121128,
 -417891.10201625]

Let's compare that to renting:


In [14]:
[rent(y * 12, 0) for y in xrange(1, 11)]


Out[14]:
[-48490.0,
 -98180.0,
 -149070.0,
 -201160.0,
 -254450.0,
 -308940.0,
 -364630.0,
 -421520.0,
 -479610.0,
 -538900.0]

So, renting becomes more expensive quickly, if we assume our money has no time value. That's not a good assumption, so let's delve a little deeper.

IRR

IRR is a common concept when evaluating investments. The IRR essentially states the smallest time value your money needs to have for you to lose money on your investment. In other words, you want to know the rate that causes your discounted cash flow to become negative.


In [15]:
def irr(dcf):
    if dcf(0) < 0: return
    for ix in xrange(1000):
        r_t = float(ix) / 1000
        if dcf(r_t) < 0:
            return r_t

Note that there are cases where there is no IRR. Your investment loses money, even assuming your upfront dollars are valued equally to later hypothetical dollars. It cannot grow your wealth.

There will be many cases where this happens with our house investment.

We can model this "investment" by subtracting the rent we're not paying from the other costs that we now must pay. Now, (drumroll please), let's look at the IRR for our hypothetical house, depending on how long we stay in it:


In [16]:
for year in xrange(1, 16):
    def dcf(r_i): return buy(12 * year, r_i / 12) - rent(12 * year, r_i / 12)
    print year, irr(dcf)


1 None
2 None
3 None
4 None
5 0.006
6 0.021
7 0.031
8 0.039
9 0.046
10 0.051
11 0.054
12 0.058
13 0.06
14 0.063
15 0.065

Yikes. We don't get above the 1.3% current risk free rate for five years. And our return never rises above the 10% average annual returns for the S&P 500.

Bring on the Bears

Of course, market conditions can change drastically and quickly. What does our investment look like in a super-pessimistic case, where our house loses half its value?


In [17]:
def buy_ohno(months, r):
    return buy_in_general(mortgage, 0.02, 0.065, 0.01, yearly_taxes, months, 0.5, r)

In [18]:
for year in xrange(1, 21):
    def dcf(r_i): return buy_ohno(12 * year, r_i / 12) - rent(12 * year, r_i / 12)
    print year, irr(dcf)


1 None
2 None
3 None
4 None
5 None
6 None
7 None
8 None
9 None
10 None
11 None
12 None
13 None
14 None
15 None
16 None
17 None
18 None
19 0.006
20 0.018

Yikes, yikes, yikes.

The Long View

If that's not masochistic enough, let's look at what house swapping in the Bay area might look like. Every time we sell our house, we'll buy another similar house somewhere else. We'll also plow all of our equity from before into the new mortgage.

In other words, our very simplistic assumption is that we essentially have the same mortgage all along. We just need to pay both buying and selling closing costs each time we switch houses.


In [19]:
for year in (1, 2, 5, 10, 25, 50):
    swap_months = year * 12
    total_months = 50 * 12
    swaps = [m for m in xrange(swap_months, total_months, swap_months)]

    def buy_and_appreciate(r): 
        return buy_in_general(mortgage, 0.01, 0.065, 0.01, yearly_taxes, total_months, 1.0, r)

    def dcf_swapping_houses(r):
        switching_costs = sum(npv(0.075 * mortgage.total, m, r / 12) for m in swaps)
        return buy_and_appreciate(r / 12) - rent(total_months, r / 12) - switching_costs

    print year, irr(dcf_swapping_houses)


1 None
2 0.013
5 0.035
10 0.05
25 0.068
50 0.078

So it looks like we're never going to beat the market here. AFAICT, there's pretty much no upside to real estate in the Bay Area. Unless you got in early and cheap, I guess.